#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Copyright (c) 2025 粤港澳大湾区（广东）国创中心

This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with this program.  If not, see <https://www.gnu.org/licenses/>.

File: PythonCodeEditor.py
Desc: A Python code editor with syntax highlighting, auto-completion, error checking, and code folding.
Author: xumanjia
Date: 2025-08-01
"""
import re
import sys
import keyword
import jedi
import tempfile
from PyQt5.QtWidgets import QApplication, QMainWindow, QMessageBox, QShortcut
from PyQt5.QtGui import QFont, QColor, QKeySequence
from PyQt5.QtCore import Qt, QTimer, QThread, pyqtSignal, QProcess
from PyQt5.Qsci import QsciScintilla, QsciLexerPython, QsciAPIs, QsciScintillaBase

class ErrorChecker(QThread):
    error_detected = pyqtSignal(list)

    def __init__(self, code):
        super().__init__()
        self.code = code

    def run(self):
        try:
            with tempfile.NamedTemporaryFile(mode='w', delete=False, suffix='.py') as f:
                f.write(self.code)
                f.flush()
                
                process = QProcess()
                process.start('pyflakes', [f.name])
                process.waitForFinished()
                
                output = process.readAllStandardError().data().decode()
                errors = self.parse_errors(output)
                self.error_detected.emit(errors)
        except Exception as e:
            print("Error checking failed:", str(e))

    def parse_errors(self, output):
        errors = []
        for raw_line in output.splitlines():
            # 尝试两种常见错误格式
            if ':' in raw_line:  # pyflakes格式
                parts = raw_line.split(':')
                if len(parts) < 4:
                    continue
                    
                try:
                    err = {
                        'line': max(0, int(parts[1].strip()) - 1),
                        'column': int(parts[2].strip()),
                        'message': ':'.join(parts[3:]).strip()
                    }
                    errors.append(err)
                except (ValueError, IndexError):
                    continue
                    
            elif re.match(r'[A-Z]\d+', raw_line):  # flake8格式
                match = re.match(r'([^:]+):(\d+):(\d+): ([A-Z]\d+) (.+)', raw_line)
                if match:
                    try:
                        err = {
                            'line': int(match.group(2)) - 1,
                            'column': int(match.group(3)),
                            'message': f"{match.group(4)}: {match.group(5)}"
                        }
                        errors.append(err)
                    except (ValueError, IndexError):
                        continue
        return errors

class PythonEditor(QsciScintilla):
    def __init__(self, parent=None):
        super().__init__(parent)
        
        # 初始化编辑器设置
        self.init_editor_config()
        self.init_lexer()
        # 自动换行，按单词换行
        self.setWrapMode(QsciScintilla.WrapCharacter)
        #self.setWrapMode(QsciScintilla.WrapWord)
        # 光标样式配置（新增部分）
        self.init_cursor_style()
        self.init_autocomplete()
        self.init_error_checking()
        self.init_code_folding()
        self.init_shortcuts()

    def init_editor_config(self):
        """基础编辑器配置"""
        self.setUtf8(True)
        self.setFont(QFont("Consolas", 11))
        self.setMarginsFont(QFont("Consolas", 9))
        
        # 行号设置、背景色
        self.setMarginType(0, QsciScintilla.NumberMargin)
        self.setMarginWidth(0, "000")
        self.setMarginLineNumbers(0, True)
        self.setMarginsBackgroundColor(QColor(245,245,245))
        # 缩进设置
        self.setIndentationsUseTabs(False)
        self.setTabWidth(4)
        self.setAutoIndent(True)
        self.setBackspaceUnindents(True)
        
        # 括号匹配
        self.setBraceMatching(QsciScintilla.SloppyBraceMatch)
        #self.setMatchedBraceBackgroundColor(QColor("#8CA58F"))
        self.setUnmatchedBraceForegroundColor(QColor("#B69292"))

    def init_lexer(self):
        """语法高亮配置"""
        self.lexer = QsciLexerPython()
        
        # 颜色方案（VS Code风格）
        #字体颜色
        self.lexer.setDefaultColor(QColor("#3D3D3D"))
        #背景颜色
        self.lexer.setDefaultPaper(QColor("#FFFFFF"))
        self.lexer.setFont(QFont("Consolas", 11))
        
        # 设置不同语法元素颜色
        color_map = {
            # 字符串相关
            QsciLexerPython.SingleQuotedString: "#CE9178",
            QsciLexerPython.DoubleQuotedString: "#CE9178",
            QsciLexerPython.TripleSingleQuotedString: "#CE9178",
            QsciLexerPython.TripleDoubleQuotedString: "#CE9178",
            QsciLexerPython.SingleQuotedFString: "#CE9178",
            QsciLexerPython.DoubleQuotedFString: "#CE9178",
            QsciLexerPython.TripleSingleQuotedFString: "#CE9178",
            QsciLexerPython.TripleDoubleQuotedFString: "#CE9178",
            
            # 其他语法元素
            QsciLexerPython.Keyword: "#569CD6",
            QsciLexerPython.ClassName: "#4EC9B0",
            QsciLexerPython.FunctionMethodName: "#DCDCAA",
            QsciLexerPython.Comment: "#6A9955",
            QsciLexerPython.CommentBlock: "#6A9955",
            QsciLexerPython.Operator: "#1B1B1B",
            QsciLexerPython.Number: "#3D3D3D",
            QsciLexerPython.Decorator: "#3D3D3D",  # 装饰器
            }
        
        for style, color in color_map.items():
            self.lexer.setColor(QColor(color), style)
        
        self.setLexer(self.lexer)

    def init_cursor_style(self):
        """配置光标视觉效果"""
        # 基础颜色
        self.setCaretForegroundColor(QColor("#585858"))  # 光标颜色
        
        # 行高亮配置
        self.setCaretLineVisible(False)                   # 启用行高亮
        #self.setCaretLineBackgroundColor(QColor("#C2C7C4"))  # 行背景色
        
        # 高级样式
        self.setCaretWidth(1)                            # 光标宽度
        self.SendScintilla(QsciScintilla.SCI_SETCARETSTYLE, 
                        QsciScintilla.CARETSTYLE_LINE)  # 竖线光标
        
        # 可选：禁用光标闪烁
        self.SendScintilla(QsciScintilla.SCI_SETCARETPERIOD, 600)  # 0表示不闪烁
        #行号背景色
        

    def init_autocomplete(self):
        """自动补全配置"""
        # 基础API补全
        self.apis = QsciAPIs(self.lexer)
        for kw in keyword.kwlist + list(dir(__builtins__)):
            self.apis.add(kw)
        self.apis.prepare()
        
        # 补全参数设置
        self.setAutoCompletionSource(QsciScintilla.AcsAPIs)
        self.setAutoCompletionCaseSensitivity(False)
        self.setAutoCompletionThreshold(1)
        self.setAutoCompletionReplaceWord(True)
        
        # Jedi智能补全
        self.completion_timer = QTimer()
        self.completion_timer.setSingleShot(True)
        self.completion_timer.timeout.connect(self.jedi_completion)
        
        # 参数提示
        self.setCallTipsStyle(QsciScintilla.CallTipsNoContext)
        self.setCallTipsVisible(0)

    def init_error_checking(self):
        """错误检查配置"""
        self.error_marker = 8
        self.markerDefine(QsciScintilla.Circle, self.error_marker)
        self.setMarkerBackgroundColor(QColor("#FF0000"), self.error_marker)
        
        # 错误检查定时器
        self.check_timer = QTimer()
        self.check_timer.setSingleShot(True)
        self.check_timer.timeout.connect(self.run_error_check)
        self.textChanged.connect(lambda: self.check_timer.start(1000))

    def init_code_folding(self):
        """代码折叠配置"""
        self.setFolding(QsciScintilla.BoxedTreeFoldStyle)
        self.lexer.setFoldComments(True)
        self.lexer.setFoldQuotes(True)
        
        # 折叠区域颜色
        self.setFoldMarginColors(
            QColor("#FFFFFF"), 
            QColor("#FFFFFF")
        )
        
        # 折叠标记图标
        self.markerDefine(QsciScintilla.Circle, QsciScintilla.SC_MARKNUM_FOLDEROPEN)
        self.markerDefine(QsciScintilla.Circle, QsciScintilla.SC_MARKNUM_FOLDER)

    def init_shortcuts(self):
        """设置代码折叠快捷键"""
        
        # 设置Ctrl+[ 切换折叠
        self.SendScintilla(QsciScintilla.SCI_ASSIGNCMDKEY,
                        ord('[') | (QsciScintilla.SCMOD_CTRL << 16),
                        QsciScintillaBase.SC_FOLDACTION_TOGGLE)
        
        # 设置Ctrl+] 展开折叠
        self.SendScintilla(QsciScintilla.SCI_ASSIGNCMDKEY,
                        ord(']') | (QsciScintilla.SCMOD_CTRL << 16),
                        QsciScintillaBase.SC_FOLDACTION_EXPAND)
        
        # 可选：设置Alt+[ 折叠所有
        self.SendScintilla(QsciScintilla.SCI_ASSIGNCMDKEY,
                        ord('[') | (QsciScintilla.SCMOD_ALT << 16),
                        QsciScintillaBase.SCI_FOLDALL)
        
        # 注释切换
        # self.shortcut_comment = self.createShortcut(QKeySequence("Ctrl+/"))
        self.shortcut_comment = QShortcut(QKeySequence(Qt.CTRL + Qt.Key_Slash), self)
        self.shortcut_comment = QShortcut(QKeySequence("Ctrl+/"), self)
        self.shortcut_comment.activated.connect(self.toggle_comment)

    def keyPressEvent(self, event):
        """重写按键事件处理"""
        super().keyPressEvent(event)
        
        # 触发智能补全的字符
        trigger_chars = ['.', ' ', '(', '[', '{', '=']
        if event.text() in trigger_chars or event.key() in [Qt.Key_Tab, Qt.Key_Return]:
            self.completion_timer.start(100)
            
        # 显示参数提示
        if event.text() == '(':
            self.show_call_tips()

    def jedi_completion(self):
        """使用Jedi进行智能补全"""
        try:
            script = jedi.Script(
                code=self.text(),
                path='untitled.py'
            )
            row, col = self.getCursorPosition()
            completions = script.complete(line=row+1, column=col)
            
            proposals = []
            for c in completions:
                item = {
                    'word': c.name,
                    'type': c.type,
                    'doc': c.docstring()[:200]  # 限制文档长度
                }
                proposals.append(item)
                
            # 显示自定义补全列表
            #if proposals:
            #    self.showCompletion("\n".join([f"{p['word']} ({p['type']})" for p in proposals]))
                
        except Exception as e:
            print("Jedi error:", str(e))

    def show_call_tips(self):
        """显示函数参数提示"""
        try:
            script = jedi.Script(
                code=self.text(),
                path='untitled.py'
            )
            row, col = self.getCursorPosition()
            signatures = script.get_signatures(row+1, col)
            
            if signatures:
                sig = signatures[0]
                tip = f"{sig.name}({', '.join(p.get_code() for p in sig.params)})"
                self.SendScintilla(QsciScintilla.SCI_CALLTIPSHOW, 
                                 self.getCursorPosition()[1], 
                                 tip.encode())
        except:
            pass

    def run_error_check(self):
        """运行错误检查"""
        if not hasattr(self, 'checker') or not self.checker.isRunning():
            self.checker = ErrorChecker(self.text())
            self.checker.error_detected.connect(self.handle_errors)
            self.checker.start()

    def handle_errors(self, errors):
        """处理错误信息"""
        self.markerDeleteAll()
        for err in errors:
            line = err['line']
            self.markerAdd(line, self.error_marker)
            self.annotate(line, err['message'])

    def annotate(self, line, message):
        """添加行尾注释"""
        self.SendScintilla(QsciScintilla.SCI_ANNOTATIONSETTEXT, line, message.encode())
        self.SendScintilla(QsciScintilla.SCI_ANNOTATIONSETVISIBLE, 2)

    def toggle_comment(self):
        """智能注释切换（支持多行、缩进保留）"""
        line_from, _, line_to, _ = self.getSelection()
        
        # 判断是否全部行已注释
        all_commented = all(
            self.text(line).lstrip().startswith("#") 
            for line in range(line_from, line_to+1)
        )
        
        # 添加/移除注释
        for line in range(line_from, line_to+1):
            text = self.text(line)
            if all_commented:
                new_text = text.replace("#", "", 1) if text.lstrip().startswith("#") else text
            else:
                indent = len(text) - len(text.lstrip())
                new_text = text[:indent] + "#" + text[indent:]
            
            self.setSelection(line, 0, line, len(text))
            self.replaceSelectedText(new_text)

    def toggle_single_line_comment(self, line):
        """单行注释切换"""
        text = self.text(line)
        if text.strip().startswith("#"):
            # 取消注释
            new_text = text.replace("#", "", 1)
        else:
            # 添加注释
            new_text = "#" + text
        self.setSelection(line, 0, line, len(text))
        self.replaceSelectedText(new_text)


class MainWindow(QMainWindow):
    def __init__(self):
        super().__init__()
        self.editor = PythonEditor()
        self.setCentralWidget(self.editor)
        self.setWindowTitle("Python Editor")
        self.resize(1000, 800)
        
        # 初始化示例代码
        self.editor.setText(
            "# 输入代码示例\n"
            "def hello_world():\n"
            "    print('Hello World!')\n"
            "\n"
            "class MyClass:\n"
            "    def __init__(self):\n"
            "        self.value = 42\n"
        )

if __name__ == "__main__":
    app = QApplication(sys.argv)
    window = MainWindow()
    window.show()
    sys.exit(app.exec_())